namespace Aphalina.Compression
{
    using System;
    using System.Collections.Generic;
    using System.IO;

    public class Deflate
    {
        private int[] backlinks = new int[0x10000];
        private int dataLength;
        private int[] literalFreq;
        private const int MaxBacklinks = 0x20;
        private const int MaxDepth = 4;
        private const int MaxOffset = 0x8000;
        private int[] offsetFreq;
        private int position;
        private int[] result = new int[0x8001];
        private int resultPosition;
        private Stream stream;
        private Dictionary<int, int> strHash = new Dictionary<int, int>();
        private byte[] window;
        private int windowLength;
        private BitWriter writer;

        public Deflate(Stream stream)
        {
            this.stream = stream;
            this.window = new byte[0x10000];
            this.writer = new BitWriter(stream);
        }

        private void CalcBacklinks()
        {
            int num = (this.position > 2) ? (this.position - 2) : (this.position + 1);
            int key = (this.window[num + 1] << 8) | this.window[num + 2];
            for (int i = num + 1; i < (this.dataLength - 3); i++)
            {
                key = ((key << 0x10) & 0xffff00) | this.window[i + 2];
                int num4 = 0;
                if (this.strHash.TryGetValue(key, out num4))
                {
                    int num5 = i - num4;
                    if (num5 < 0x8000)
                    {
                        this.backlinks[i] = num5;
                    }
                    this.strHash[key] = i;
                }
                else
                {
                    this.strHash[key] = i;
                }
            }
        }

        private void CalcCodeLength(int[] codeLengths, int codeCount, List<int> result, List<int> repeats)
        {
            int item = -1;
            int index = 0;
            while (index < codeCount)
            {
                int num;
                int num2;
                int num3 = 1;
                int num6 = codeLengths[index];
                if (num6 == 0)
                {
                    num = 0x8a;
                    num2 = 3;
                }
                else
                {
                    num = 6;
                    num2 = 3;
                    if (item != num6)
                    {
                        result.Add(num6);
                        repeats.Add(0);
                        num3 = 0;
                    }
                }
                item = num6;
                index++;
                while ((index < codeCount) && (item == codeLengths[index]))
                {
                    index++;
                    if (++num3 >= num)
                    {
                        break;
                    }
                }
                if (num3 < num2)
                {
                    if (num3 > 0)
                    {
                        result.Add(item);
                        repeats.Add(0);
                    }
                    if (num3 > 1)
                    {
                        result.Add(item);
                        repeats.Add(0);
                    }
                }
                else if (item != 0)
                {
                    result.Add(0x10);
                    repeats.Add(num3);
                }
                else
                {
                    if (num3 <= 10)
                    {
                        result.Add(0x11);
                        repeats.Add(num3);
                        continue;
                    }
                    result.Add(0x12);
                    repeats.Add(num3);
                }
            }
        }

        private int CheckBestString(StringInfo baseString)
        {
            StringInfo info = this.FindBestString(baseString);
            int length = info.Position - baseString.Position;
            if (length > 2)
            {
                this.WriteOffset(baseString.Offset, length);
            }
            else
            {
                for (int i = baseString.Position; i < info.Position; i++)
                {
                    this.WriteLiteral(this.window[i]);
                }
            }
            this.WriteOffset(info.Offset, info.Length);
            return (info.Position + info.Length);
        }

        private void Encode(bool isLastBlock)
        {
            this.dataLength = this.position + 0x8000;
            if ((this.dataLength > this.windowLength) || isLastBlock)
            {
                this.dataLength = this.windowLength;
            }
            this.CalcBacklinks();
            this.resultPosition = 0;
            this.literalFreq = new int[0x11e];
            this.offsetFreq = new int[30];
            new StringInfo();
            while (this.position < (this.dataLength - 2))
            {
                StringInfo baseString = this.FindString(this.position);
                if (baseString.Length < 3)
                {
                    this.WriteLiteral(this.window[this.position++]);
                }
                else
                {
                    this.position = this.CheckBestString(baseString);
                }
            }
            if (this.position < this.dataLength)
            {
                this.WriteLiteral(this.window[this.position++]);
            }
            if (this.position < this.dataLength)
            {
                this.WriteLiteral(this.window[this.position++]);
            }
            this.WriteLiteral(0x100);
            HuffmanTree literalTree = new HuffmanTree(this.writer, this.literalFreq);
            HuffmanTree offsetTree = new HuffmanTree(this.writer, this.offsetFreq);
            this.writer.WriteBits(isLastBlock ? 1 : 0, 1);
            this.writer.WriteBits(2, 2);
            this.WriteTree(literalTree, offsetTree);
            for (int i = 0; i < this.resultPosition; i++)
            {
                if ((this.result[i] & 0x40000000) != 0)
                {
                    int length = (this.result[i] & 0x7fff) + 3;
                    int lengthSlot = DeflateConstants.GetLengthSlot(length);
                    literalTree.WriteCode(lengthSlot + 0x101);
                    if (DeflateConstants.ExtraLengthBits[lengthSlot] > 0)
                    {
                        this.writer.WriteBits(length - DeflateConstants.LiteralLengthBase[lengthSlot], DeflateConstants.ExtraLengthBits[lengthSlot]);
                    }
                    int pos = (this.result[i] >> 15) & 0x7fff;
                    int slot = DeflateConstants.GetSlot(pos);
                    offsetTree.WriteCode(slot);
                    if (DeflateConstants.ExtraDistanceBits[slot] > 0)
                    {
                        this.writer.WriteBits((pos + 1) - DeflateConstants.DistancePositionBase[slot], DeflateConstants.ExtraDistanceBits[slot]);
                    }
                }
                else
                {
                    literalTree.WriteCode(this.result[i]);
                }
            }
            if (isLastBlock)
            {
                this.writer.Flush();
            }
        }

        public void Encode(byte[] buffer, int offset, int count)
        {
            while (count > 0)
            {
                if (this.windowLength < this.window.Length)
                {
                    int length = count;
                    if (length > (this.window.Length - this.windowLength))
                    {
                        length = this.window.Length - this.windowLength;
                    }
                    Array.Copy(buffer, offset, this.window, this.windowLength, length);
                    count -= length;
                    this.windowLength += length;
                    offset += length;
                }
                else
                {
                    this.Encode(false);
                    if (this.position > 0x8000)
                    {
                        int num2 = this.position - 0x8000;
                        if (num2 > 0)
                        {
                            Array.Copy(this.window, 0x8000, this.window, 0, num2);
                            this.position = 0x8000;
                            this.windowLength = num2;
                            Array.Copy(this.backlinks, 0x8000, this.backlinks, 0, num2);
                            Dictionary<int, int> strHash = this.strHash;
                            this.strHash = new Dictionary<int, int>();
                            foreach (int num3 in strHash.Keys)
                            {
                                int num4 = strHash[num3] - 0x8000;
                                if (num4 > 0)
                                {
                                    this.strHash.Add(num3, num4);
                                }
                            }
                            continue;
                        }
                    }
                }
            }
        }

        private StringInfo FindBestString(StringInfo baseString)
        {
            StringInfo info = baseString;
            int basePosition = baseString.Position + 1;
            int num2 = 1;
            if (baseString.Length != 0x102)
            {
                while (((num2 < baseString.Length) && (num2 < 4)) && (basePosition < (this.dataLength - 2)))
                {
                    StringInfo info2 = this.FindString(basePosition);
                    if (info.Length < (info2.Length - ((num2 < 4) ? num2 : 4)))
                    {
                        info = info2;
                    }
                    num2++;
                    basePosition++;
                }
            }
            return info;
        }

        private StringInfo FindString(int basePosition)
        {
            StringInfo info2 = new StringInfo {
                Position = basePosition
            };
            StringInfo info = info2;
            int index = basePosition;
            for (int i = 0; ((info.Length < 0x102) && (this.backlinks[index] != 0)) && ((((basePosition - index) + this.backlinks[index]) < 0x8000) && (i < 0x20)); i++)
            {
                int num3 = basePosition;
                int num4 = index - this.backlinks[index];
                int num5 = 0;
                while ((num5 < 0x102) && (num3 < this.dataLength))
                {
                    if (this.window[num3] != this.window[num4])
                    {
                        break;
                    }
                    num3++;
                    num4++;
                    num5++;
                }
                if (info.Length < num5)
                {
                    info.Length = num5;
                    info.Offset = (basePosition - index) + this.backlinks[index];
                }
                index -= this.backlinks[index];
            }
            return info;
        }

        public void Finish()
        {
            this.Encode(true);
        }

        private void WriteLiteral(int literal)
        {
            this.result[this.resultPosition] = literal;
            this.resultPosition++;
            this.literalFreq[literal]++;
        }

        private void WriteOffset(int offset, int length)
        {
            this.result[this.resultPosition] = (0x40000000 | (length - 3)) | ((offset - 1) << 15);
            this.resultPosition++;
            int lengthSlot = DeflateConstants.GetLengthSlot(length);
            this.literalFreq[lengthSlot + 0x101]++;
            int slot = DeflateConstants.GetSlot(offset - 1);
            this.offsetFreq[slot]++;
        }

        private void WriteTree(HuffmanTree literalTree, HuffmanTree offsetTree)
        {
            List<int> result = new List<int>(0x13b);
            List<int> repeats = new List<int>(0x13b);
            this.CalcCodeLength(literalTree.CodeLength, literalTree.MaxCode, result, repeats);
            this.CalcCodeLength(offsetTree.CodeLength, offsetTree.MaxCode, result, repeats);
            int[] freq = new int[0x13];
            for (int i = 0; i < result.Count; i++)
            {
                freq[result[i]]++;
            }
            HuffmanTree tree = new HuffmanTree(this.writer, freq);
            int num2 = literalTree.MaxCode - 0x101;
            this.writer.WriteBits((num2 > 0) ? num2 : 0, 5);
            int num3 = offsetTree.MaxCode - 1;
            this.writer.WriteBits((num3 > 0) ? num3 : 0, 5);
            this.writer.WriteBits(tree.MaxCode - 4, 4);
            for (int j = 0; j < tree.MaxCode; j++)
            {
                int bits = tree.CodeLength[DeflateConstants.HuffmanCodeLengthOrder[j]];
                this.writer.WriteBits(bits, 3);
            }
            for (int k = 0; k < result.Count; k++)
            {
                tree.WriteCode(result[k]);
                int num7 = repeats[k];
                if (num7 > 0)
                {
                    if (result[k] == 0x10)
                    {
                        this.writer.WriteBits(num7 - 3, 2);
                    }
                    else if (result[k] == 0x11)
                    {
                        this.writer.WriteBits(num7 - 3, 3);
                    }
                    else
                    {
                        this.writer.WriteBits(num7 - 11, 7);
                    }
                }
            }
        }

        private class StringInfo
        {
            public int Length;
            public int Offset;
            public int Position;
        }
    }
}

